﻿// Copyright (C) 2013  Patrick Maher

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using Paragon.Geometry;
using Paragon.Math;

namespace Paragon.Core
{
    public sealed class ParagonEngine
    {
        private Octree _bvh;
        private Viewport _viewPort;
        private BufferedGraphics _frameBuffer;

        // TODO: Volume size should be implicitly set by level of scene detail and size.
        // If one volume exceeds addressing limit we can figure out a way to break them
        // up into blocks of memory.
        public int _volumeSize = 1024;

        public int CameraPosition { get; set; }
        private bool _stopRequested;

        private Font _stringFont = new Font("Arial", 10);
        private SolidBrush _stringBrush = new SolidBrush(Color.Yellow);

        // TODO: Move the framerate counter to it's own component.
        // Start writing to a screen overlay.
        private Stopwatch _framerateTimer;
        private Stopwatch _updateTimer;
        private int _frameRate;
        private int _frameCount;

        private Bitmap _frame;
        private Graphics _frameGraphicsContext;
        BitmapData _bmpData;
        Rectangle _rect;
        AutoResetEvent _stopSignal;
        public MovementState MoveState { get; set; }
        private int _cornFlower = Color.CornflowerBlue.ToArgb();
        private Vector3f _coords = new Vector3f();
        private Ray _r = new Ray();

        public ParagonEngine(Graphics deviceContext)
        {
            LoadBvh();
            InitializeGraphicsBuffer(deviceContext);
            InitializeCamera();

            _framerateTimer = new Stopwatch();
            _updateTimer = new Stopwatch();
            _frame = new Bitmap(_viewPort.ScreenWidth, _viewPort.ScreenHeight);
            _frameGraphicsContext = Graphics.FromImage(_frame);
            _rect = new Rectangle(0, 0, _volumeSize, _volumeSize);
            _stopSignal = new AutoResetEvent(false);

            MoveState = MovementState.Idle;
        }

        private void LoadBvh()
        {
            _bvh = new Octree(NodeAddress.TreeDepthFromVolumeSize(_volumeSize));
            _bvh.Load(Paragon.Properties.Settings.Default.Images);
        }

        private void InitializeCamera()
        {
           // _viewPort = new Viewport(new Vector3f(256, 256, 1), new Vector3f(416, 376, 400), 240, 320);
           // _viewPort = new Viewport(new Vector3f(600, 500, 500), new Vector3f(416, 376, 600), 240, 320);
            //_viewPort = new Viewport(new Vector3f(512, 512, 500), new Vector3f(352, 390, 690), 240, 320);
            _viewPort = new Viewport(new Vector3f(512, 512, 1000), new Vector3f(352, 390, 1190), 240, 320);

            //Camera camera = new Camera(new Vector3f(512, 512, 1000), new Vector3f(352, 390, 1190), 640, 480, 0.5f);
        }

        private void InitializeGraphicsBuffer(Graphics deviceContext)
        {
            BufferedGraphicsContext bufferContext = BufferedGraphicsManager.Current;

            // TODO: We should set this to the screen size.
            _frameBuffer = bufferContext.Allocate(deviceContext, new Rectangle(0 ,0, _volumeSize, _volumeSize));
        }

        public void Start()
        {
            Thread renderThread = new Thread(new ThreadStart(RenderLoop));
            renderThread.IsBackground = false;
            renderThread.Name = "RenderLoop";

            renderThread.Start();
            _framerateTimer.Start();
            _updateTimer.Start();
        }
        private void RenderLoop()
        {
            while (!_stopRequested)
            {
                Update();
                DrawFrame();
                DrawFramerate();
                RenderScene();
                // TODO End scene to flush buffer and clear screen.
            }

            _stopRequested = false;

            EndScene();
        }

        public void Stop()
        {
            _stopRequested = true;
        }

        public unsafe void Insert(Bitmap image, int z)
        {
            int height, width;
            if (image.Height > _volumeSize)
            {
                height = _volumeSize;
            }
            else
            {
                height = image.Height;
            }
            if (image.Width > _volumeSize)
            {
                width = _volumeSize;
            }
            else
            {
                width = image.Width;
            }

            BitmapData surfaceData = image.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            //Thread topThread = new Thread(new ParameterizedThreadStart(WriteTop));
            //Thread bottomThread = new Thread(new ParameterizedThreadStart(WriteBottom));
            //Thread writeThread = new Thread(new ParameterizedThreadStart(Write));            

            //topThread.Start(surfaceData);
            //bottomThread.Start(surfaceData);
            AutoResetEvent eventSignal = new AutoResetEvent(false);
            WriteThreadParameter writeParams = new WriteThreadParameter(surfaceData, z, eventSignal);
            //writeThread.Start(writeParams);

            ThreadPool.QueueUserWorkItem(new WaitCallback(Write), writeParams);
            eventSignal.WaitOne();

            //topThread.Join();
            //bottomThread.Join();
            //writeThread.Join();

            image.UnlockBits(surfaceData);
        }

        // TODO: Just do a BitBlt here for now until a good parallel
        // process is figured out.
        private unsafe void Write(object parameters)
        {
            WriteThreadParameter writeParams = (WriteThreadParameter)parameters;

            int* row = (int*)writeParams.BitmapData.Scan0;
            //int incrementor = 0;
            //if (_width < (surfaceData.Stride/4))
            //{
            //    incrementor = (surfaceData.Stride / 4) - _width;
            //}

            for (int y = 0; y < writeParams.BitmapData.Height; y++)
            {
                for (int x = 0; x < writeParams.BitmapData.Width; x++)
                {
                    //TopScene.Insert(NodeAddress.VectorToZOrderCurve(x, y, Z), row[0]);
                    _bvh.Insert(row[0], NodeAddress.VectorToZOrderCurve(x, y, writeParams.TreeDepth));
                    row++;
                }

                //row += incrementor;
            }

            writeParams.ResetEvent.Set();
        }

        // Give this a little more precision.
        public void Update()
        {
            if (_framerateTimer.ElapsedMilliseconds > 1000)
            {
                _frameRate = _frameCount;
                _frameCount = 0;
                _framerateTimer.Restart();
            }

            if (_updateTimer.ElapsedMilliseconds > 66)
            {
                switch (MoveState)
                {
                    case MovementState.Forward:
                        CameraPosition++;
                        _viewPort.CameraOrigin.Z++;
                        _viewPort.ScreenOrigin.Z++;
                        break;
                    case MovementState.Back:
                        if (_viewPort.CameraOrigin.Z != 0)
                        {
                            _viewPort.CameraOrigin.Z--;
                            _viewPort.ScreenOrigin.Z--;
                        }                        
                        break;
                    case MovementState.Left:
                        break;
                    case MovementState.Right:
                        break;
                    default:
                        break;
                }

                _updateTimer.Restart();
            }
        }

        public unsafe void DrawFrame()
        {
            _bmpData = _frame.LockBits(new Rectangle(0, 0, _viewPort.ScreenWidth, _viewPort.ScreenHeight), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

            _stopSignal.Reset();
            ReadThreadParameter readParams = new ReadThreadParameter(_bmpData, _stopSignal);
            ThreadPool.QueueUserWorkItem(new WaitCallback(Read), readParams);
            _stopSignal.WaitOne();

            _frame.UnlockBits(_bmpData);

            _frameBuffer.Graphics.DrawImage(_frame, 0, 0);
        }

        private unsafe void Read(object parameters)
        {
            ReadThreadParameter readParams = (ReadThreadParameter)parameters;
            int* row = (int*)readParams.BitmapData.Scan0;
            int color = 0;
            float position = 1.0f;

            for (int y = 0; y < _viewPort.ScreenHeight; y++)
            {
                for (int x = 0; x < _viewPort.ScreenWidth; x++)
                {
                    _r = _viewPort.GetRay(x, y);
                    color = 0;
                    position = 1.0f;
                    for (int i = 0; i < 256; i++)
                    {
                        PMath.Lerp(_r.Origin, _r.Direction, position, ref _coords);
                        position++;
                        color = _bvh.Read(NodeAddress.VectorToZOrderCurve(_coords));
                        if (color == -1)
                        {
                            continue;
                        }
                        else
                        {
                            break;
                        }
                    }

                    if (color == -1)
                    {
                        color = _cornFlower;
                    }
                    row[0] = color;
                    row++;
                }
            }


            // -10185235

            readParams.ResetEvent.Set();
        }

        public void DrawFramerate()
        {
            _frameBuffer.Graphics.DrawString(_frameRate.ToString() + " frames per second", _stringFont, _stringBrush, 0.0f, 0.0f);
        }

        public void RenderScene()
        {
            _frameBuffer.Render();

            _frameCount++;
        }

        public void EndScene()
        {
            _stringFont.Dispose();
            _stringBrush.Dispose();

            _frame.Dispose();
            _frameBuffer.Graphics.Clear(Color.CornflowerBlue);
            _frameBuffer.Render();
            _frameBuffer.Dispose();
        }
    }
}